fix: deliver OSC notifications when hooks have no controlling tty#44
Open
ThomasCrul wants to merge 1 commit into
Open
fix: deliver OSC notifications when hooks have no controlling tty#44ThomasCrul wants to merge 1 commit into
ThomasCrul wants to merge 1 commit into
Conversation
When the plugin's hooks run inside a sandbox that detaches the subprocess
from its controlling terminal (e.g. Claude Code's macOS sandbox-exec
wrapper for Bash tool calls, opted into via `sandbox.enabled` in
`.claude/settings.json`), `open("/dev/tty")` returns ENXIO and every
notification silently fails. Fall back to walking the process tree and
writing the OSC sequence directly to the first ancestor tty that accepts
the bytes — typically the un-sandboxed `claude` process in the Warp pane.
- Add `find-controlling-tty.sh` exposing `find_candidate_ttys`, which
prints each ancestor's tty device path (one per line), skipping
ancestors with no controlling tty. Depth-capped at 20 hops.
- Update both `warp-notify.sh` (structured) and `legacy/warp-notify.sh`
to try `/dev/tty` first and iterate candidates on failure.
- Add `test-find-controlling-tty.sh` with 5 cases (direct-parent hit,
walk-past-?? ancestors, multiple-ancestor emission, no-tty-anywhere,
depth-limit termination), mocking `ps` as a shell function.
Behavior is unchanged for users not running in a tty-detached sandbox.
skspade
added a commit
to skspade/claude-code-warp
that referenced
this pull request
May 13, 2026
skspade
added a commit
to skspade/claude-code-warp
that referenced
this pull request
May 13, 2026
Resolution: took pr-27's version of on-post-tool-use.sh and test-hooks.sh. This sacrifices warpdotdev#35's PostToolUse-specific perf inline-jq optimization (which used a direct /dev/tty write, bypassing warpdotdev#44's tty walker) in favor of using the patched warp-notify.sh helper, plus gains tool_preview and permission_mode fields.
skspade
added a commit
to skspade/claude-code-warp
that referenced
this pull request
May 13, 2026
… Warp UI is unresponsive Resolution: hybrid of warpdotdev#44's tty walker and warpdotdev#37's timeout watchdog. Each candidate tty is now tried with a TIMEOUT_SEC watchdog (default 2s) so a hung Warp UI on one tty falls through to the next ancestor instead of blocking forever.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix notifications when hooks have no controlling tty
Summary
When the plugin's hooks run inside a sandbox that detaches the subprocess from its controlling terminal,
/dev/ttybecomes unopenable —open("/dev/tty")returns ENXIO because the process is in a new session with no controlling tty. Everyprintf > /dev/ttyinwarp-notify.shthen silently fails, so no OSC reaches Warp and the agent indicators (blue dot, in-progress / blocked badges, completion toasts) never show.The concrete trigger is Claude Code's macOS
sandbox-execwrapper for Bash tool calls, opted into via"sandbox": {"enabled": true}in.claude/settings.json(a sandbox feature it added recently). Other sandboxers (firejail, etc.) that detach the controlling tty would hit the same failure.This PR makes the notify path resilient: when
/dev/ttyis unavailable, walk the process tree and try each ancestor's tty in turn. The first one that accepts the write (typicallyclaudeitself, which lives in the Warp pane and isn't sandboxed) is used.Behavior is unchanged for users not running in a tty-detached sandbox.
Repro
Requires macOS + Claude Code + Warp.
.claude/settings.json:{ "sandbox": { "enabled": true } }claudeinside Warp from that project directory.SessionStart,Stop,Notification,UserPromptSubmit,PostToolUse) all fire and callwarp-notify.sh, but Warp shows no agent indicator and no completion toast.Instrumenting
warp-notify.shto capture the redirection error shows:Writing to the parent process's actual pty (e.g.
/dev/ttys003) succeeds and Warp displays the indicator.Changes
plugins/warp/scripts/find-controlling-tty.sh— exposesfind_candidate_ttys, which walks$PPIDupward viaps -o tty=/ps -o ppid=and prints each ancestor's tty device path (one per line), skipping ancestors with no controlling tty. Depth-capped at 20 hops to bound the walk.plugins/warp/scripts/warp-notify.sh(structured) — try/dev/ttyfirst; on failure, iteratefind_candidate_ttysand write to each until one accepts the bytes.plugins/warp/scripts/legacy/warp-notify.sh— same fallback.plugins/warp/tests/test-find-controlling-tty.sh— 5 cases: direct-parent hit, walk-past-??-ancestors, multiple-ancestor emission (nested case), no-tty-anywhere, depth-limit termination. Mockspsas a shell function.Existing
tests/test-hooks.shcontinues to pass (40/40); new tests add 5 more.Caveats
pscost. Worst case is 2 calls per hop × 20 hops = 40psinvocations; in practice 2-4 calls. Sub-millisecond on modern hardware.claudeitself runs in the Warp pane and is reachable via the tree walk.ps -o tty=output format. macOS reportsttys003, Linux reportspts/3; both prefix correctly to/dev/.... Not tested on other Unixes (the plugin targets Warp, which is macOS / Linux only).Relation to other PRs
warp-notify.shpaths share it, emits all writable candidates (so a failed write to one ancestor falls through to the next, useful in nested setups), updates both scripts, and adds dedicated unit tests for the helper. Happy to defer to fix: TTY detection for hook subprocesses #19 if preferred — flagging this purely so the duplication is visible.WARP_CLI_AGENT_IPCwhen Warp exposes it. That env var isn't set on current stable Warp (v0.2026.05.06.15.42.stable_05) on my machine, so it doesn't help today, but it's the cleaner long-term channel and this PR would naturally fall back to it once Warp sets it (the/dev/ttypath still gets tried first; if both fail, the ancestor walk runs)./dev/ttywrites against UI hangs) is unrelated but in the same general area.Test plan
bash plugins/warp/tests/test-hooks.sh— 40/40 passbash plugins/warp/tests/test-find-controlling-tty.sh— 5/5 passsandbox.enabled: truein.claude/settings.json, freshclaudein Warp shows agent indicators (blue dot, in-progress badge, completion toast). With sandbox off, behavior is unchanged.